/*
 * This class serves as a simple client for audio chat.
 * Upon execution, it asks for a target host ip address to which it sends audio recorded from a mic 
 * and from which it receives recorded audio.
 * 
 * Threads:			rThread: calls record()
 * 					sThread: repeatedly calls send();
 * 					pThread: repeatedly calls playBack();
 * 
 * Major methods:	
 * 					record()
 * 						-starts recording audio from mic, and starts sThread
 * 					send()
 * 						-called by sThread, sends audio to remote host
 * 					listen()	
 *						-starts receiving audio from remote host and plays back
 * 					playBack(ByteArrayOutputStream baos)
 * 						-plays audio from remote host	
 * 					playBack()
 * 						-FOR TESTING PURPOSES ONLY (used to play locally recorded audio)
 * 
 * 
 */

import java.io.*;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import javax.sound.sampled.*;

public class AudioChat {
	static ByteArrayOutputStream outgoing;
	static ByteArrayOutputStream incoming;
	static AudioFormat format;
	static float sampleRate;
	static int sampleSizeInBits;
	static int channels;
	static boolean signed;
	static boolean bigEndian;

	static String ip;
	static int count;

	static pThread p;
	static rThread r;
	static sThread s;

	public static void main(String[] args) throws IOException {

		count = 1;

		// specify audio format
		sampleRate = 8000;
		sampleSizeInBits = 8;
		channels = 1;
		signed = true;
		bigEndian = true;
		format = new AudioFormat(sampleRate, sampleSizeInBits, channels,
				signed, bigEndian);

		BufferedReader stdIn = new BufferedReader(new InputStreamReader(
				System.in));

		System.out.println("Input target host IP (i.e. xxx.xxx.xxx.xxx)");

		ip = stdIn.readLine();

		r = new rThread();
		r.start();

		listen();
	}

	public static void startSession(String s) throws IOException {
		ip = s;
		count = 1;

		// specify audio format
		sampleRate = 8000;
		sampleSizeInBits = 8;
		channels = 1;
		signed = true;
		bigEndian = true;
		format = new AudioFormat(sampleRate, sampleSizeInBits, channels,
				signed, bigEndian);

		//starts recording
		r = new rThread();
		r.start();
		
		//starts listening and playing back
		listen();
	}

	static class rThread extends Thread {
		public void run() {
			record();
		}
	}

	static class sThread extends Thread {
		public void run() {
			while (true) {
				try {
					send();
				} catch (InterruptedException e) {
					System.err.println("Interrupted exception");
					System.exit(-1);
				}
			}
		}
	}

	static class pThread extends Thread {
		public void run() {
			try {
				while (true) {
					playBack(incoming);
				}
			} catch (IOException e) {
				System.err.println("I/O exception: unable to playback");
				System.exit(-1);
			}
		}
	}

	private static void record() {
		DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
		TargetDataLine line = null;
		try {
			line = (TargetDataLine) AudioSystem.getLine(info);
			line.open(format);
		} catch (LineUnavailableException e) {
			System.err.println("Line unavailable");
			System.exit(-1);
		}

		// starts capturing
		line.start();

		int bufferSize = (int) format.getSampleRate() * format.getFrameSize();

		byte buffer[] = new byte[bufferSize];
		outgoing = new ByteArrayOutputStream();
		int c = 1;
		while (c <= 0) {
			int count = line.read(buffer, 0, buffer.length);
			if (count > 0) {
				outgoing.write(buffer, 0, count);
				System.out.println("Now recording for " + c + " seconds");
			}
			c++;
		}

		sThread s = new sThread();
		s.start();

		while (true) {
			int count = line.read(buffer, 0, buffer.length);
			if (count > 0) {
				outgoing.write(buffer, 0, count);
				System.out.println("Now recording for " + c + " seconds");
			}
			c++;
		}
	}

	private static void send() throws InterruptedException {
		byte audio[] = outgoing.toByteArray();
		outgoing.reset();

		int byteToSend = audio.length;
		int remain = audio.length;

		InetAddress address = null;
		try {
			address = InetAddress.getByName(ip);
		} catch (UnknownHostException e) {
			System.err.println("Unknown host");
			System.exit(-1);
		}

		// Open a datagram socket
		DatagramSocket socket = null;
		try {
			socket = new DatagramSocket();
		} catch (SocketException e) {
			System.err.println("Socket exception: unable to open socket");
			System.exit(-1);
		}

		socket.connect(address, 55556);

		while (remain > 0) {
			if (remain > 512) {
				DatagramPacket packet = new DatagramPacket(audio, byteToSend
						- remain, 512);
				try {
					socket.send(packet);
					System.out.println("Packet #" + count + " sent");
					Thread.currentThread().sleep(50);
					remain -= 512;
					System.out.println("remaining bytes: " + remain);
					count++;
				} catch (IOException e) {
					System.err.println("I/O exception: unable to send packet");
					System.exit(-1);
				}
			} else {
				DatagramPacket packet = new DatagramPacket(audio, byteToSend
						- remain, remain);
				try {
					socket.send(packet);
					System.out.println("Packet sent");
					System.out.println("Packet #" + count + " sent");
					Thread.currentThread().sleep(50);
					remain = 0;
				} catch (IOException e) {
					System.err.println("I/O exception: unable to send packet");
					System.exit(-1);
				}
			}
		}
		socket.close();
	}

	/**
	 * Accepts audio from a remote host and plays it
	 * 
	 * @throws IOException
	 */
	private static void listen() throws IOException {

		incoming = new ByteArrayOutputStream(512);

		DatagramSocket socket = null;

		try {
			socket = new DatagramSocket(55556);
		} catch (SocketException e) {
			System.err.println("Socket exception: unable to open socket");
			System.exit(-1);
		}

		byte[] temp = new byte[512];

		int counter = 0;

		while (counter < 5) {
			DatagramPacket packet = new DatagramPacket(temp, temp.length);

			try {
				System.out.println("Now listening...");
				socket.receive(packet);
				incoming.write(packet.getData());
				System.out.println("Packet received, count = " + counter);
			} catch (IOException e) {
				System.err.println("Unable to receive packet");
				System.exit(-1);
			}
			counter++;
		}

		p = new pThread();
		p.start();

		while (true) {
			DatagramPacket packet = new DatagramPacket(temp, temp.length);

			try {
				System.out.println("Waiting for packet");
				socket.receive(packet);
				incoming.write(packet.getData());
				System.out.println("Packet received, count = " + counter);
			} catch (IOException e) {
				System.err.println("Unable to receive packet");
				System.exit(-1);
			}
			counter++;
		}
	}

	/**
	 * Plays audio from a ByteArrayOutputStream
	 * 
	 * @param baos
	 * @throws IOException
	 */
	private static void playBack(ByteArrayOutputStream baos) throws IOException {

		byte buffer[] = baos.toByteArray();
		baos.reset();

		AudioInputStream ais = new AudioInputStream(new ByteArrayInputStream(
				buffer), format, buffer.length / format.getFrameSize());

		DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
		SourceDataLine pline = null;
		try {
			pline = (SourceDataLine) AudioSystem.getLine(info);
			pline.open(format);
		} catch (LineUnavailableException e) {
			System.err.println("Line unavailable");
			System.exit(-1);
		}

		pline.start();

		int count;
		while ((count = ais.read(buffer, 0, buffer.length)) != -1) {
			if (count > 0) {
				pline.write(buffer, 0, count);
			}
		}
		pline.drain();
		pline.close();
	}

	/**
	 * For playback on local machine
	 * 
	 * @throws IOException
	 */
	private static void playBack() throws IOException {
		byte audio[] = outgoing.toByteArray();
		outgoing.reset();
		AudioInputStream ais = new AudioInputStream(new ByteArrayInputStream(
				audio), format, audio.length / format.getFrameSize());

		DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
		SourceDataLine pline = null;
		try {
			pline = (SourceDataLine) AudioSystem.getLine(info);
			pline.open(format);
		} catch (LineUnavailableException e) {
			System.err.println("Line unavailable");
			System.exit(-1);
		}

		pline.start();

		int bufferSize = (int) format.getSampleRate() * format.getFrameSize();
		byte buffer[] = new byte[bufferSize];

		int count;
		while ((count = ais.read(buffer, 0, buffer.length)) != -1) {
			if (count > 0) {
				pline.write(buffer, 0, count);
			}
		}
		pline.drain();
		pline.close();
	}
}
